Saltar al contenido principal

7.2.5 POO + Ficheros

En proyectos reales no guardamos variables sueltas, guardamos objetos de dominio (Producto, Usuario, Pedido, etc.). El reto es persistir esos objetos sin mezclar lógica de negocio con lógica de archivos.

La clave de esta unidad es aprender a conectar POO + persistencia de forma limpia, mantenible y escalable.


1️⃣ Por qué combinar POO con persistencia​

Cuando trabajas con clases, los datos viven en memoria mientras el programa está abierto. Si quieres conservar el estado entre ejecuciones, necesitas escribirlos en disco (JSON, CSV, base de datos...).

Problema típico:

  • En memoria tienes objetos (Producto(...)).
  • En disco se guardan estructuras serializables (dict, list, str, etc.).

Por tanto, necesitas una transformación controlada entre ambos mundos.


2️⃣ Patrón de transformación (to_dict / from_dict)​

La forma más común de persistir objetos en JSON es convertir cada objeto a diccionario al guardar y reconstruirlo al cargar. Esta operación la haremos con los métodos to_dict y from_dict, no son métodos obligatorios de Python ni del módulo json. Son una convención de diseño muy extendida en proyectos reales porque hacen explícita la traducción:

  • Objeto Python -> estructura serializable (dict) con to_dict().
  • Estructura serializable (dict) -> objeto Python con from_dict().

Es decir, son un patrón recomendado, no una regla del lenguaje.

Importante

to_dict / from_dict no son métodos mágicos (dunder).
Son métodos normales definidos por diseño para mantener la persistencia bajo control.

🟩 Modelo de clase persistente​

Clase preparada para persistencia
class Producto:
def __init__(self, sku, nombre, precio, stock=0):
self.sku = sku
self.nombre = nombre
self.precio = precio
self.stock = stock

def to_dict(self):
return {
"sku": self.sku,
"nombre": self.nombre,
"precio": self.precio,
"stock": self.stock,
}

@classmethod
def from_dict(cls, datos):
return cls(
sku=datos["sku"],
nombre=datos["nombre"],
precio=datos["precio"],
stock=datos.get("stock", 0),
)
en este ejemplo
  • to_dict() define el formato de salida del objeto.
  • from_dict() centraliza la reconstrucción del objeto.
  • Usar get("stock", 0) evita fallos con archivos de versiones antiguas.

🟨 Comparativa rápida​

EnfoqueVentajaRiesgo
Guardar atributos sueltos en cualquier parteRápido al inicioCaos al crecer el proyecto
to_dict / from_dict en la claseConsistencia y mantenibilidadRequiere disciplina de diseño

3️⃣ Separación de responsabilidades: clase repositorio​

La clase de dominio (Producto) no debería abrir ni cerrar archivos. Esa tarea va en una clase de persistencia (repositorio, manager o DAO).

🟩 Cómo se implementa un repositorio (paso a paso)​

Un repositorio mínimo suele seguir este diseño:

  1. __init__(ruta) guarda la ubicación del fichero.
  2. cargar() lee el fichero y devuelve objetos de dominio.
  3. guardar(lista_objetos) serializa y persiste en disco.
  4. (Opcional) métodos de conveniencia como buscar_por_id, actualizar, eliminar.

La idea central es que el resto del programa no sepa nada de open(), json.load() o json.dump(). Solo trabaja con objetos y llama al repositorio.

🟦 Repositorio básico de inventario​

Repositorio JSON
import json

class InventarioRepositorio:
def __init__(self, ruta):
self.ruta = ruta

def guardar(self, productos):
datos = [p.to_dict() for p in productos]
with open(self.ruta, "w", encoding="utf-8") as f:
json.dump(datos, f, indent=4, ensure_ascii=False)

def cargar(self):
try:
with open(self.ruta, "r", encoding="utf-8") as f:
datos = json.load(f)
return [Producto.from_dict(d) for d in datos]
except (FileNotFoundError, json.JSONDecodeError):
return []

🟨 Contrato recomendado del repositorio​

  • cargar() siempre devuelve una colección válida (normalmente lista, aunque esté vacía).
  • guardar(...) recibe entidades de dominio y no diccionarios crudos.
  • El repositorio captura errores de infraestructura habituales (FileNotFoundError, JSONDecodeError) y deja la lógica de negocio limpia.

Este contrato reduce acoplamiento y facilita pruebas.

🟧 Ventaja arquitectónica​

  • La clase Producto se centra en reglas de negocio.
  • El repositorio se centra en E/S de archivos.
  • Cambiar de JSON a BD impacta menos al resto del sistema.

4️⃣ Identificadores únicos y consistencia de objetos​

Si no controlas identificadores, aparecen duplicados y datos inconsistentes.

🟩 Búsqueda y actualización por sku​

Actualizar por identificador único
def actualizar_precio(productos, sku_buscado, nuevo_precio):
for p in productos:
if p.sku == sku_buscado:
p.precio = nuevo_precio
return True
return False

🟦 Control básico de alta duplicada​

Evitar duplicados
def agregar_producto(productos, nuevo):
if any(p.sku == nuevo.sku for p in productos):
return False
productos.append(nuevo)
return True
Regla práctica

En persistencia basada en ficheros, define siempre un campo clave (id, sku, uuid) antes de implementar altas/ediciones.


5️⃣ Estrategias de guardado​

No existe una única estrategia correcta; depende del riesgo de pérdida y del volumen de cambios.

🟩 Guardado por evento​

Guardar tras cada cambio relevante.

  • Más seguro frente a cierres inesperados.
  • Puede penalizar rendimiento si hay muchas operaciones.

🟦 Guardado al final​

Guardar una sola vez al terminar.

  • Rápido durante la ejecución.
  • Mayor riesgo de perder cambios si hay fallo.

🟨 Auto-guardado por lote​

Guardar cada X operaciones o cada X segundos.

  • Equilibrio entre seguridad y rendimiento.
  • Muy útil en aplicaciones de escritorio o CLI largas.

🟧 Comparativa de estrategia​

EstrategiaSeguridad de datosRendimientoUso típico
Por eventoAltaMedio/BajoSistemas críticos
Al finalBajaAltaScripts pequeños
Por loteMedia/AltaMedia/AltaApps interactivas

6️⃣ Evolución de esquema y compatibilidad​

Con el tiempo, tus clases cambian (stock, categoria, activo, etc.). Si no preparas compatibilidad, los archivos antiguos dejarán de cargar.

🟩 Compatibilidad hacia atrás con valores por defecto​

from_dict defensivo
@classmethod
def from_dict(cls, datos):
return cls(
sku=datos["sku"],
nombre=datos["nombre"],
precio=datos["precio"],
stock=datos.get("stock", 0),
)

🟦 Migraciones simples por versión​

Migración mínima de datos
def migrar_producto(datos):
version = datos.get("_version", 1)

if version == 1:
datos["stock"] = 0
datos["_version"] = 2

return datos
Recomendación

Si tu modelo va a evolucionar, añade desde el principio un campo _version en el formato persistido.


7️⃣ Ejemplo completo: servicio + repositorio​

Este ejemplo muestra el ciclo mínimo real: cargar estado, operar con objetos y persistir cambios.

Flujo completo de inventario
repo = InventarioRepositorio("inventario.json")
productos = repo.cargar()

ok = agregar_producto(productos, Producto("SKU-900", "Webcam Pro", 69.9, 12))
if ok:
repo.guardar(productos)

Flujo recomendado: cargar -> operar en objetos -> validar -> guardar.


✅ Buenas prácticas recomendadas​

  • Mantén to_dict / from_dict en cada entidad persistible.
  • No mezcles E/S dentro de tus clases de dominio.
  • Define identificadores únicos y valida duplicados.
  • Gestiona compatibilidad de esquema con .get() y versiones.
  • Usa escritura segura (tmp + reemplazo) cuando los datos sean críticos.

🧪 Ejercicios prácticos – Proyecto Atlas​

El equipo de operaciones necesita migrar su inventario a una arquitectura orientada a objetos con persistencia en JSON. Tu misión es reconstruir el módulo de inventario usando clases, repositorio y validaciones de consistencia.

Dispones de estos ficheros: 👉 Descárgalos aquí

  • inventario_base.json -> estado inicial del catálogo
  • movimientos_stock.json -> entradas/salidas de unidades
  • inventario_legacy.json -> datos de versión antigua sin stock

🟢 Fase 1 – Modelo y carga inicial​

🟩 Ejercicio 1 – Clase Producto persistible​

Crea la clase Producto con:

  • Atributos: sku, nombre, precio, stock
  • Método to_dict()
  • Método de clase from_dict()

Objetivo: definir el contrato de transformación objeto <-> diccionario.

🟦 Ejercicio 2 – Repositorio de inventario​

Implementa InventarioRepositorio con:

  • cargar()
  • guardar(lista_productos)

Usa inventario_base.json como fuente inicial.

Objetivo: separar dominio y persistencia.

🟡 Fase 2 – Operaciones de negocio​

🟨 Ejercicio 3 – Alta segura de productos​

Implementa una función para añadir productos sin duplicar sku.

  • Si el sku existe, no se añade.
  • Si no existe, se añade y devuelve éxito.

Objetivo: proteger integridad del inventario.

🟧 Ejercicio 4 – Aplicar movimientos de stock​

Lee movimientos_stock.json y aplica cada operación:

  • ENTRADA -> suma unidades
  • SALIDA -> resta unidades (sin dejar stock negativo)

Genera inventario_actualizado.json.

Objetivo: trabajar con objetos en memoria y persistencia final.

🔵 Fase 3 – Evolución de esquema​

🟥 Ejercicio 5 – Cargar datos legacy​

Usa inventario_legacy.json, donde no existe stock, y cárgalo sin errores.

Condición:

  • from_dict() debe asignar stock = 0 por defecto.

Objetivo: mantener compatibilidad hacia atrás.

🟫 Ejercicio 6 – Migración con versión​

Crea un script que lea inventario_legacy.json y genere inventario_v2.json con:

  • Campo stock
  • Campo _version con valor 2

Objetivo: practicar migración explícita de datos.

🔴 Fase Final – Informe operativo​

🟪 Ejercicio 7 – Informe de consistencia (informe_inventario.txt)​

Genera un informe con:

  • Total de productos.
  • Total de SKUs duplicados detectados.
  • Valor total del inventario (precio * stock).
  • Productos con stock crítico (menor que 3).

Formato sugerido:

Productos totales: X
SKUs duplicados: X
Valor inventario: XXXX.XX
Stock crítico: SKU-001, SKU-010